/**
 * This file is a part of qloudgen-sec. 
 * You can redistribute qloudgen-sec and/or modify it under the terms of the Lesser GNU General Public License version 3. 
 * qloudgen-sec is distributed WITHOUT ANY WARRANTY. 
 *
 * See the Lesser GNU General Public License for more details at http://www.gnu.org/licenses/. 
 **/
package org.qloudgen.sec.shiro;

import java.util.Map;

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.subject.Subject.Builder;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionKey;
import org.apache.shiro.util.Factory;
import org.apache.shiro.util.ThreadContext;

import io.buji.pac4j.ClientToken;
import org.pac4j.cas.credentials.CasCredentials;
import org.pac4j.cas.profile.CasProfile;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.qloudgen.sec.util.StringUtil;
import org.qloudgen.sec.util.ExceptionUtil;
import org.qloudgen.sec.shiro.client.CasClient;
import org.qloudgen.sec.shiro.permission.DroolsPermission;
import org.qloudgen.sec.shiro.session.TicketSessionIdGenerator;

/**
 * <p>Title: Security Implementation</p>
 * <p>Description: Wrappered shiro authentication and authorization implementation for osgi service.</p>
 * <p>Copyright: qloudgen/sec 2014</p>
 * @author <a href="mailto:hyq.dd.sun@gmail.com">glue.3M</a>
 */
@SuppressWarnings( { "serial" , "unchecked" } )
public final class SecurityUtil {
	/**
	 * Log output.
	 */
	private final static Logger logger = LoggerFactory.getLogger( SecurityUtil.class );
	/**
	 *
	 */
	private final static Object lock = new Object();
	/**
	 * A VM (static) singleton instance.
	 */	
	private static SecurityUtil securityUtil = null;
	/**
	 * Current security manager instance.
	 *
	 * This will simplify get security manager because SecurityUtils.getSecurityManager() will check ThreadContext.
	 */
	private DefaultSecurityManager securityManager = null;
	
	/**
	 * Custom constructor.
	 *
	 * @param config
	 */
	private SecurityUtil(String config) {
		/**
		 * First we need load security framework config.
		 */
		Factory<SecurityManager> factory = null;
		if ( StringUtil.isBlank( config ) ) {
			factory = new IniSecurityManagerFactory();
		}
		else {
			factory = new IniSecurityManagerFactory( config );
		}
		/**
		 * Make sure this will be set because shiro always use SecurityUtils get security manager.
		 *
		 * 1)We do not bind security manager to shiro thread context.
		 * 2)All we need is a global security manager instance.
		 */
		SecurityUtils.setSecurityManager( factory.getInstance() );
		securityManager = ( DefaultSecurityManager )SecurityUtils.getSecurityManager();
		//
		logger.info( "Current security security manager instance is...{}" , securityManager );
	}
	
	/**
	 * Returns current security util instance.
	 *
	 * Default security config file will lookup this path "classpath:shiro.ini".
	 *
	 * @return SecurityUtil
	 */
	public static SecurityUtil getInstance() {
		return getInstance( System.getProperty( "qloudgen.shiro" , null ) );
	}
	
	/**
	 * Returns current security util instance.
	 *
	 * @param config
	 * @return SecurityUtil
	 */
	public static SecurityUtil getInstance(String config) {
		//
		if ( securityUtil != null ) {
			return securityUtil;
		}
		//
		synchronized ( lock ) {
			if ( securityUtil == null ) {
				securityUtil = new SecurityUtil( config );
			}
			return securityUtil;
		}
	}
	
	/**
	 * Destory current security util instance.
	 */
	public void destroy() {
		//
		try {
			/**
			 * First destroy current security manager.
			 *
			 * This will destory all cache manager, realm manager, session manager, authorizer manager, authenticator manager except
			 * All these managers not implement org.apache.shiro.util.Destroyable interface.
			 *
			 * At the same time the manager will destory all the cache, realm, session, authorizer, authenticator except
			 * All these instances not implement org.apache.shiro.util.Destroyable interface.
			 */
			securityManager.destroy();
		}
		catch (Exception e) {
			logger.error( "Current security manager destory error...\n" , ExceptionUtil.get( e ) );
		}
		finally {
			/**
			 * Finally we do need force to shutdown all instance to support restart.
			 */
			synchronized ( lock ) {
				if ( securityUtil != null ) {
					securityUtil = null;
				}
			}
		}
	}
	
	/**
	 * Returns subject.
	 *
	 * @param ticket
	 * @return Subject
	 */
	private Subject getSubject(final String ticket) {
		/**
		 * Do create.
		 */
		Subject subject = ( new Subject.Builder() ).sessionId( ticket ).buildSubject();
		//
		if ( subject.isAuthenticated() ) {
			logger.warn( "Current proxy user is authenticated..." );
		}
		else {
			logger.warn( "Current proxy user is never authenticated..." );
		}
		//
		return subject;
	}
	
	/**
	 * Login.
	 * 
	 * Ticket has expired time from external container to osgi container.
	 *
	 * @param ticket
	 */
	public void login(final String ticket) {
		//
		try {
			//
			logger.info( "Starting proxy authentication..." );
			/**
			 * Do create subject..
			 */
			Subject subject = getSubject( ticket );
			if ( subject.isAuthenticated() ) {
				return;
			}
			/**
			 * Do bind current thread context session id.
			 */
			ThreadContext.put( TicketSessionIdGenerator.SESSION_ID , ticket );
			/**
			 * Do create token.
			 */
			AuthenticationToken token = new ClientToken( CasClient.CLIENT_NAME , new CasCredentials( ticket , CasClient.CLIENT_NAME ) );
			/**
			 * Do really authentication.
			 */
			subject.login( token );
			//
			logger.info( "Started proxy authentication...{}" , subject.getPrincipals() );
		}
		catch (Throwable t) {
			logger.error( "Current proxy authentication is fail...\n{}" , ExceptionUtil.get( t ) );
			throw new AuthenticationException( t );
		}
		finally {
			/**
			 * Do unbind current thread context session id.
			 */
			ThreadContext.remove( TicketSessionIdGenerator.SESSION_ID );
		}
	}
	
	/**
	 * Logout.
	 *
	 * Must be called before web application logut.
	 *
	 * @param ticket
	 */
	public void logout(final String ticket) {
		//
		try {
			//
			logger.info( "Starting proxy logout..." );
			/**
			 * Do create subject..
			 */
			Subject subject = getSubject( ticket );
			if ( ! subject.isAuthenticated() ) {
				return;
			}
			/**
			 * Do really logout.
			 */
			subject.logout();
			//
			logger.info( "Started proxy logout..." );
		}
		catch (Throwable t) {
			logger.error( "Current proxy logout is fail...\n{}" , ExceptionUtil.get( t ) );
		}
	}
	
	/**
	 * Authorization.
	 *
	 * @param ticket
	 * @param name
	 * @param facts
	 */
	public boolean isPermitted(final String ticket , final String name , final Object[] facts) {
		//
		try {
			//
			logger.info( "Starting proxy authorization..." );
			/**
			 * Do create subject..
			 */
			Subject subject = getSubject( ticket );
			if ( ! subject.isAuthenticated() ) {
				return false;
			}
			/**
			 * Do really authorization.
			 */
			boolean result = subject.isPermitted( new DroolsPermission( name , facts ) );
			//
			logger.info( "Started proxy authorization...{}" , result );
			//
			return result;
		}
		catch (Throwable t) {
			logger.error( "Current proxy authorization is fail...\n{}" , ExceptionUtil.get( t ) );
			return false;
		}
	}
	
	/**
	 * Default shiro will always create session with subject.
	 * 1)Can use ( new Subject.Builder() ).sessionId( ticket ).buildSubject().isAuthenticated() test current user.
	 * 2)Can driectly access session manager test current user.
	 *
	 * @see AbstractSessionDAO.readSession(Serializable sessionId)
	 *
	 * @param ticket
	 * @return boolean
	 */
	public boolean isAuthenticated(final String ticket) {
		//
		try {
			//if ( securityManager.getSession( new DefaultSessionKey( ticket ) ) != null ) {
			//	return true;
			//}
			//else {
			//	return false;
			//}
			//
			return getSubject( ticket ).isAuthenticated();
		}
		catch (Throwable t) {
			logger.error( "\n{}" , ExceptionUtil.get( t ) );
			return false;
		}
	}
	
	/**
	 * Session.
	 *
	 * @param ticket
	 * @return Session
	 */
	public Session getSession(final String ticket) {
		//
		try {
			//
			logger.info( "Starting proxy session..." );
			/**
			 * Do create subject..
			 */
			Subject subject = getSubject( ticket );
			if ( ! subject.isAuthenticated() ) {
				return null;
			}
			/**
			 * Do retrieve session.
			 */
			Session session = subject.getSession( false );
			//
			logger.info( "Started proxy session...{}" , session );
			//
			return session;
		}
		catch (Throwable t) {
			logger.error( "Current proxy session is fail...\n{}" , ExceptionUtil.get( t ) );
			return null;
		}
	}
	
	/**
	 * Returns current user id.
	 *
	 * @param ticket
	 * @return String
	 */
	public String getUserId(final String ticket) {
		CasProfile profile = getUserProfile( ticket );
		return ( profile == null ? null : profile.getId() );
	}
	
	/**
	 * Returns current user attributes.
	 *
	 * @param ticket
	 * @return Map<String,Object>
	 */
	public Map<String,Object> getUserAttributes(final String ticket) {
		CasProfile profile = getUserProfile( ticket );
		return ( profile == null ? null : profile.getAttributes() );
	}
	
	/**
	 * Returns current user profile.
	 *
	 * @param ticket
	 * @return CasProfile
	 */
	private CasProfile getUserProfile(final String ticket) {
		//
		try {
			/**
			 * Do create subject..
			 */
			Subject subject = getSubject( ticket );
			if ( ! subject.isAuthenticated() ) {
				return null;
			}
			/**
			 * Do get current authenticated user principals by shiro.
			 */
			PrincipalCollection principals = subject.getPrincipals();
			logger.trace( "Current principals is...{}" , principals );
			/**
			 * Do get current authenticated user profile by pac4j.
			 */
			CasProfile profile = principals.oneByType( CasProfile.class );
			logger.trace( "Current profile is...{}" , profile );
			//
			return profile;
		}
		catch (Throwable t) {
			logger.error( "Current proxy retrieve is fail...\n{}" , ExceptionUtil.get( t ) );
			return null;
		}
	}
}